<?php

/**
 * Maikuro Framework
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2010, Jonathan Hedrén
 * @author     Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

class AnnotationsParser
{
    /**
     *
     * @var array
     */
    protected $classAnnotationsCache = array();

    /**
     *
     * @var array
     */
    protected $methodAnnotationsCache = array();

    /**
     *
     * @var array
     */
    protected $propertyAnnotationsCache = array();

    /**
     *
     * @var array
     */
    protected $reflectionClassCache = array();

    /**
     * Returns any annotations for the supplied class (or object).
     *
     * @param string $class
     * @return array
     */
    public function getClassAnnotations($class)
    {
        if (is_object($class)) {
            $class = get_class($class);
        }
        if (!isset($this->classAnnotationsCache[$class])) {
            $docComment = $this->getReflectionClass($class)
                               ->getDocComment();
            $this->classAnnotationsCache[$class] = $this->parse($docComment);
        }
        return $this->classAnnotationsCache[$class];
    }

    /**
     * Returns any method annotations. If the $method parameter is supplied,
     * only the annotations of that method will be returned, else all
     * annotations for all class methods will be returned.
     *
     * @param string|StdClass $class
     * @param string $method
     * @return array
     */
    public function getMethodAnnotations($class, $method = null)
    {
        if ($method === null) {
            $reflectionsMethods = $this->getReflectionClass($class)
                                       ->getMethods();

            if (!isset($this->methodAnnotationsCache[$class])) {
                $this->methodAnnotationsCache[$class] = array();
            }

            $annotations = array();
            foreach ($reflectionsMethods as $reflectionMethod) {
                $methodName = $reflectionMethod->getName();

                if (!isset($this->methodAnnotationsCache[$class][$methodName])) {
                    $this->methodAnnotationsCache[$class][$methodName] = $this->parse($reflectionMethod->getDocComment());
                }

                $annotations[$methodName] = $this->methodAnnotationsCache[$class][$methodName];
            }
            return $annotations;
        }

        if (!isset($this->methodAnnotationsCache[$class][$method])) {
            $docComment = $this->getReflectionClass($class)
                               ->getMethod($method)
                               ->getDocComment();
            if (!isset($this->methodAnnotationsCache[$class])) {
                $this->methodAnnotationsCache[$class] = array();
            }
            $this->methodAnnotationsCache[$class][$method] = $this->parse($docComment);
        }
        return $this->methodAnnotationsCache[$class][$method];
    }

    /**
     * Returns any property annotations. If the $property parameter is supplied,
     * only the annotations of that property will be returned, else all
     * annotations of all class properties will be returned.
     *
     * @param string|StdClass $class
     * @param string $property
     * @return array
     */
    public function getPropertyAnnotations($class, $property = null)
    {
        if (is_object($class)) {
            $class = get_class($class);
        }

        if ($property === null) {
            $reflectionsProperties = $this->getReflectionClass($class)
                                          ->getProperties();

            if (!isset($this->propertyAnnotationsCache[$class])) {
                $this->propertyAnnotationsCache[$class] = array();
            }

            $annotations = array();
            foreach ($reflectionsProperties as $reflectionProperty) {
                $propertyName = $reflectionProperty->getName();

                if (!isset($this->propertyAnnotationsCache[$class][$propertyName])) {
                    $this->propertyAnnotationsCache[$class][$propertyName] = $this->parse($reflectionProperty->getDocComment());
                }

                $annotations[$propertyName] = $this->propertyAnnotationsCache[$class][$propertyName];
            }
            return $annotations;
        }

        if (!isset($this->propertyAnnotationsCache[$class][$property])) {
            $docComment = $this->getReflectionClass($class)
                               ->getProperty($property)
                               ->getDocComment();
            if (!isset($this->propertyAnnotationsCache[$class])) {
                $this->propertyAnnotationsCache[$class] = array();
            }
            $this->propertyAnnotationsCache[$class][$property] = $this->parse($docComment);
        }
        return $this->propertyAnnotationsCache[$class][$property];
    }

    /**
     * Returns (a cached) reflection of the $class.
     *
     * @param string|StdClass $class
     * @return ReflectionClass
     */
    protected function getReflectionClass($class)
    {
        if (!isset($this->reflectionClassCache[$class])) {
            $this->reflectionClassCache[$class] = new ReflectionClass($class);
        }
        return $this->reflectionClassCache[$class];
    }

    /**
     * Checks if a class has a specific annotation.
     *
     * @param string $class
     * @param string $annotation
     * @return boolean
     */
    public function hasClassAnnotation($class, $annotation)
    {
        $annotations = $this->getClassAnnotations($class);
        return isset($annotations[$annotation]) ? true : false;
    }

    /**
     * Checks if a property has a specific annotations.
     *
     * @param string|StdClass $class
     * @param string $property
     * @param string $annotation
     * @return boolean
     */
    public function hasPropertyAnnotation($class, $property, $annotation)
    {
        $annotations = $this->getPropertyAnnotations($class, $property);
        return isset($annotations[$annotation]) ? true : false;
    }

    /**
     * Checks if a method has a specific annotation.
     *
     * @param string|StdClass $class
     * @param string $method
     * @param string $annotation
     * @return boolean
     */
    public function hasMethodAnnotation($class, $method, $annotation)
    {
        $annotations = $this->getMethodAnnotations($class, $method);
        return isset($annotations[$annotation]) ? true : false;
    }

    /**
     * Parses the doc comment.
     *
     * @param string $docComment
     * @return array
     */
    protected function parse($docComment)
    {
        $pattern = '/@(\w+)?(?:\((?:"(.+)"|(.+))\))*.*$/m';
        preg_match_all($pattern, $docComment, $matches, PREG_SET_ORDER);
        $annotations = array();
        foreach ($matches as $match) {
            if (isset($match[3])) {
                $value = $this->convertValue($match[3]);
            } else if (isset($match[2])) {
                $value = $this->convertValue($match[2]);
            } else {
                $value = true;
            }
            $annotations[$match[1]] = $value;
        }
        return $annotations;
    }

    /**
     * Converts values to their "real" type.
     *
     * @param string $value
     * @return mixed
     */
    protected function convertValue($value)
    {
        if ($value == 'true') {
            return true;
        }

        if ($value == 'false') {
            return false;
        }

        if ($value == 'null') {
            return null;
        }

        if (filter_var($value, FILTER_VALIDATE_INT)) {
            return (integer) $value;
        }

        if (filter_var($value, FILTER_VALIDATE_FLOAT)) {
            return (double) $value;
        }

        if (defined($value)) {
            return constant($value);
        }

        if (preg_match_all('/(\w+)\s*=\s*(?:"([\w\d\s]*)"|(true|false)|([\d\.]+))/', $value, $matches, PREG_SET_ORDER)) {
            $value = array();
            foreach ($matches as $match) {
               if (isset($match[4])) {
                   $value[$match[1]] = $this->convertValue($match[4]);
               } else if (isset($match[3])) {
                   $value[$match[1]] = $this->convertValue($match[3]);
               } else {
                   $value[$match[1]] = $match[2];
               }
            }
            return $value;
        }

        return $value;
    }
}